package osslog

import (
	"bufio"
	"errors"
	"io"
	"log"
	"os"
	"sync/atomic"
	"time"

	"github.com/sirupsen/logrus"
	"github.com/xuelang-group/suanpan-appconnector/pkg/storage"
	"gopkg.in/natefinch/lumberjack.v2"
)

const OSS_LOG_HELPER_DEFAULT_INTERVAL_SECOND int = 1
const OSS_LOG_HELPER_DEFAULT_PATH string = "/tmp/osslog"
const OSS_SINGLE_FILE_LIMIT int = 2
const OSS_MAX_FILES int = 10

type ossWriter struct {
	ossLogHelper *OssLogHelper
}

// 仅用来检测日志有写入,通知OssLog更新
func (w *ossWriter) Write(p []byte) (n int, err error) {
	w.ossLogHelper.UpdateLogByString(string(p))
	return len(p), nil
}

type ossPusher func(localPath string, remotePath string)

type OssLogHelper struct {
	lastWorkTime           time.Time
	timePushIntervalSecond int
	ossPath                string
	localPath              string
	PushFunc               ossPusher
	fileHasChange          int32
	uploadToOss            bool
	autoPushToOssByTime    bool
}

var (
	fileIsUploading int32
)

const (
	FILE_HAS_CHANGE = 1
	FILE_NO_CHANGE  = 0

	FILE_IS_UPLOADING    = 1
	FILE_IS_FREE_TO_EDIT = 0
)

func init() {
	log.SetFlags(0)
}

func NewOssLogHelper(pushInterval, sizeLimit, maxfiles int, ossPath, localPath string, autoPushToOssByTime, uploadToOSS bool) (*OssLogHelper, error) {
	if sizeLimit == 0 {
		sizeLimit = OSS_SINGLE_FILE_LIMIT
	}
	if maxfiles == 0 {
		maxfiles = OSS_MAX_FILES
	}
	logrus.Infof("oss log helper init, uploadToOss:%t, autoPushToOssByTime:%t, ossPath:%s, localpath:%s, sizeLimit:%d mb, maxfiles:%d",
		uploadToOSS, autoPushToOssByTime, ossPath, localPath, sizeLimit, maxfiles)

	if ossPath == "" || localPath == "" {
		return nil, errors.New("please check oss path")
	}

	ossLog := &OssLogHelper{
		lastWorkTime:           time.Now(),
		timePushIntervalSecond: pushInterval,
		ossPath:                ossPath,
		localPath:              localPath,
		PushFunc:               pushToOss,
		fileHasChange:          0, //use atomic
		autoPushToOssByTime:    autoPushToOssByTime,
		uploadToOss:            uploadToOSS,
	}

	lw := &lumberjack.Logger{
		Filename:   localPath,
		MaxSize:    sizeLimit, // megabytes
		MaxBackups: maxfiles,
	}

	ow := &ossWriter{
		ossLogHelper: ossLog,
	}

	output := io.MultiWriter(os.Stdout, lw, ow)
	log.SetOutput(output)

	if autoPushToOssByTime && uploadToOSS {
		ticker := time.NewTicker(time.Duration(int64(pushInterval)) * time.Second)
		go func() {
			for range ticker.C {
				if ossLog.fileNeedUpload() {
					ossLog.PushFunc(localPath, ossPath)
				}
			}
		}()
	}
	return ossLog, nil
}

func pushToOss(localPath string, remotePath string) {
	oss := storage.GetOssInstance()
	storageParam := []storage.StorageParemeter{map[string]string{
		"ContentType":  "text/plain; charset=utf-8",
		"Expires":      "0",
		"CacheControl": "no-cache, must-revalidate",
	}}

	//solution for uploading file when file is being edited
	atomic.StoreInt32(&fileIsUploading, FILE_IS_UPLOADING)
	defer atomic.StoreInt32(&fileIsUploading, FILE_IS_FREE_TO_EDIT)

	err := oss.FPutObject(remotePath, localPath, storageParam...)
	if err != nil {
		logrus.Errorf("push oss log to oss has err %s", err.Error())
	}

	//heng: not use in sh-solver sitution
	//heng: from chenqing:
	//if SP_OSSLOG_EXT exist
	//push another log to OSS
	//WARNING: maybe this is not a good idea
	//if config.GetEnv().SpOssLogExt != "" && config.GetEnv().SpEntry == common.MACHINE_LEARNING_ENTRY {
	//	err := oss.FPutObject(remotePath+"_"+config.GetEnv().SpOssLogExt, localPath, storageParam...)
	//	if err != nil {
	//		logrus.Errorf("push oss log to oss has err %s", err.Error())
	//	}
	//}
}

func (o *OssLogHelper) fileNeedUpload() bool {
	return atomic.CompareAndSwapInt32(&o.fileHasChange, FILE_HAS_CHANGE, FILE_NO_CHANGE)
}

// Note:
// heng cannot call this function concurrently
func (o *OssLogHelper) UpdateLogByString(runeStr string) {
	atomic.StoreInt32(&o.fileHasChange, FILE_HAS_CHANGE)
	if o.uploadToOss && !o.autoPushToOssByTime && (time.Since(o.lastWorkTime)) >= time.Duration(o.timePushIntervalSecond)*time.Second {
		o.lastWorkTime = time.Now()
		if o.fileNeedUpload() {
			go o.PushFunc(o.localPath, o.ossPath)
		}
	}
}

func (o *OssLogHelper) ReadCurrentLogToList() []string {
	var buffer []string
	fp, err := os.Open(o.localPath)
	if err != nil {
		return nil
	}
	// File is already open we use the scanner on it.
	scanner := bufio.NewScanner(fp)
	for scanner.Scan() {
		text := scanner.Text()
		buffer = append(buffer, text)
	}
	if err := scanner.Err(); err != nil {
		return nil
	}
	return buffer
}

func ReadLogFileToString(filePath string, lastBytes int) string {
	content, err := os.ReadFile(filePath)
	if err != nil {
		return ""
	}

	if len(content) > lastBytes {
		return string(content[len(content)-lastBytes:])
	}
	return string(content)
}
